﻿/*
Bf3Rcon.NET, provides a .NET implementation of BF3's RCON interface.
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3rcon.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.ComponentModel;

namespace System.Net.Battlefield3.Server
{
    /// <summary>
    /// Provides a server implementation of the RCON protocol.
    /// </summary>
    /// <remarks>
    /// This class is made to imitate the basic protocol used by Battlefield 3 servers. The only RCON features build in are
    /// for logging in and enabling events. This class allows the sending of events and responding to requests.<para />
    /// This is mainly only useful for testing RCON clients and creating a bouncer/relay (similarly to PRoCon).
    /// </remarks>
    public class RconServer
        : MarshalByRefObject, IDisposable
    {
        TcpListener Listener;
        List<RconServerClient> Clients = new List<RconServerClient>();
        int ClientId = 0;
        int Sequence = 0;
        object SequenceLock = new object();

        /// <summary>
        /// Creates a new <see cref="RconServer"/> instance that will listen on the specified <see cref="IPAddress"/> and port.
        /// </summary>
        /// <param name="address">The <see cref="IPAddress"/> that this server will listen on.</param>
        /// <param name="port">The port that this server will listen on.</param>
        public RconServer(IPAddress address, int port)
        {
            Address = address;
            Port = port;
        }

        #region Properties
        /// <summary>
        /// Gets the <see cref="IPAddress"/> that this server will listen on.
        /// </summary>
        /// <value>The <see cref="IPAddress"/> the server will listen on.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Stop"/> and <see cref="Start"/>.</remarks>
        public IPAddress Address { get; private set; }

        int _Port;
        /// <summary>
        /// Gets the port that this server will listen on.
        /// </summary>
        /// <value>The port that this server will listen on.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Stop"/> and <see cref="Start"/>.</remarks>
        /// <exception cref="ArgumentOutOfRangeException">The port must be between 0 and 65535.</exception>
        public int Port
        {
            get { return _Port; }
            private set
            {
                if (value < 0 || value > 65535) throw new ArgumentOutOfRangeException("value", "The port must be between 0 and 65535.");
                _Port = value;
            }
        }

        /// <summary>
        /// Gets the password used for clients to log on.
        /// </summary>
        /// <value>The password used for clients to log on.</value>
        public string Password { private get; set; }

        /// <summary>
        /// Gets whether or not the <see cref="RconServer"/> is listening for clients.
        /// </summary>
        /// <value>True if the <see cref="RconServer"/> is listening for clients.</value>
        public bool Listening { get { return Listener != null; } }
        #endregion

        #region Async Socket Methods
        static void Accept(RconServer server)
        {
            if (server == null) return;

            server.Listener.BeginAcceptTcpClient(AcceptCallback, server);
        }

        static void AcceptCallback(IAsyncResult ar)
        {
            RconServer server = ar.AsyncState as RconServer;
            if (server == null) return;

            try
            {
                RconServerClient newClient = new RconServerClient(server, server.Listener.EndAcceptTcpClient(ar), server.ClientId++);
                server.Clients.Add(newClient);

                Receive(newClient);
            }
            catch (SocketException)
            {
                //TODO: event maybe
                //i assume this will happen when the listener is stopped
                return;
            }
            catch (ObjectDisposedException) //possible rare race condition
            {
                return;
            }

            Accept(server);
        }

        static void Receive(RconServerClient client)
        {
            if (!client.IsConnected) return;

            ReadState<RconServerClient> state = new ReadState<RconServerClient>(client);
            try
            {
                client.Socket.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReceiveCallback, state);
            }
            catch (InvalidOperationException) //socket is disposed (thru disconnect)
            {
                return;
            }
        }

        static void ReceiveCallback(IAsyncResult ar)
        {
            ReadState<RconServerClient> state = (ReadState<RconServerClient>)ar.AsyncState;
            RconServerClient client = state.Client;

            if (!client.IsConnected) return;

            int bytesRead = 0;
            try
            {
                bytesRead = client.Socket.EndReceive(ar);
            }
            catch (SocketException)
            {
                client.Disconnect();
                return;
            }

            if (bytesRead > 0)
            {
                for (int i = 0; i < bytesRead; i++)
                    client.LeftoverData.Add(state.Buffer[i]);

                if (!client.IsConnected) return;

                //gets all packets here to avoid problems with modifying LeftoverData
                List<Packet> packets = new List<Packet>(5); //random capacity. probably won't go past this
                while (Packet.ContainsCompletePacket(client.LeftoverData))
                {
                    packets.Add(new Packet(new List<byte>(client.LeftoverData), client.LeftoverData));
                }

                ThreadPool.QueueUserWorkItem(new WaitCallback(HandlePacketReceive),
                    new ParsePacketState<RconServerClient>(client, packets));
            }
            else //connection is closed
            {
                client.Disconnect();
                return; //stop the next receive
            }

            Receive(client);
        }

        static void Send(RconServerClient client, Packet data)
        {
            client.Socket.BeginSend(data.ToByteArray(), 0, data.NumberOfBytes, 0, SendCallback, client);
        }

        static void SendCallback(IAsyncResult ar)
        {
            try
            {
                ((RconServerClient)ar.AsyncState).Socket.EndSend(ar);
            }
            catch (ObjectDisposedException)
            {
                return;
            }
        }
        #endregion

        #region Events
        #region Event Helpers
        //these methods invoke the events, providing the special features mentioned above
        static void InvokeEvent<TArgs>(EventHandler<TArgs> eventHandler, object sender, TArgs e) where TArgs : EventArgs
        {
            if (eventHandler == null) return;

            foreach (EventHandler<TArgs> ev in eventHandler.GetInvocationList())
            {
                ISynchronizeInvoke target = ev.Target as ISynchronizeInvoke;

                try
                {
                    if (target != null && target.InvokeRequired)
                        target.Invoke(ev, new object[] { sender, e });
                    else
                        ev(sender, e);
                }
                catch (AppDomainUnloadedException)
                {
                    eventHandler -= ev;
                }
            }
        }
        //static void InvokeEvent(EventHandler eventHandler, object sender)
        //{
        //    if (eventHandler == null) return;

        //    foreach (EventHandler ev in eventHandler.GetInvocationList())
        //    {
        //        ISynchronizeInvoke target = ev.Target as ISynchronizeInvoke;

        //        try
        //        {
        //            if (target != null && target.InvokeRequired)
        //                target.Invoke(ev, new object[] { sender, EventArgs.Empty });
        //            else
        //                ev(sender, EventArgs.Empty);
        //        }
        //        catch (AppDomainUnloadedException)
        //        {
        //            eventHandler -= ev;
        //        }
        //    }
        //}
        #endregion



        //event will only be singlecast so there's no conflict with the response
        EventHandler<RequestEventArgs> _Request;
        /// <summary>
        /// Occurs when the <see cref="RconServer"/> receives a request from a client.
        /// </summary>
        /// <remarks>
        /// Only one event handler is allowed. Attempting to add more than one will overwrite the existing one.
        /// </remarks>
        public event EventHandler<RequestEventArgs> Request
        {
            add
            {
                _Request = value;
            }
            remove
            {
                _Request = null;
            }
        }

        /// <summary>
        /// Raises the <see cref="Request"/> event of the <see cref="RconServer"/>.
        /// </summary>
        /// <param name="e">A <see cref="RequestEventArgs"/> that contains the event data.</param>
        protected void OnRequest(RequestEventArgs e)
        {
            InvokeEvent(_Request, this, e);
        }
        #endregion

        #region Send Methods
        //original used to easily match up sequence
        //response is only being used for its words
        static void SendResponse(RconServerClient client, Packet original, IEnumerable<string> words)
        {
            string[] wordArray = words as string[] ?? words.ToArray();

            Send(client, new Packet(false, true, original.Sequence, wordArray));
        }

        static void SendResponse(RconServerClient client, Packet original, params string[] words)
        {
            SendResponse(client, original, words as IEnumerable<string>);
        }

        /// <summary>
        /// Asynchronously sends an event to all connected clients.
        /// </summary>
        /// <param name="words">The words that will be sent to the clients.</param>
        public void SendEvent(params string[] words)
        {
            List<RconServerClient> clientsToRemove = new List<RconServerClient>(Clients.Count);

            int sequence = GetSequence();
            foreach (RconServerClient client in Clients)
            {
                if (!client.IsConnected)
                {
                    clientsToRemove.Add(client);
                    continue;
                }

                if (client.EventsEnabled) Send(client, new Packet(true, false, sequence, words));
            }

            foreach (RconServerClient client in clientsToRemove)
            {
                Clients.Remove(client);
            }
        }

        int GetSequence()
        {
            lock (SequenceLock)
            {
                Sequence = (Sequence + 1) & 0x3fffffff;
                return Sequence;
            }
        }
        #endregion

        #region Start and Stop
        /// <summary>
        /// Starts the <see cref="RconServer"/>.
        /// </summary>
        /// <exception cref="SocketException">The exception is thrown by the <see cref="TcpListener"/>.</exception>
        public void Start()
        {
            if (!Listening)
            {
                Listener = new TcpListener(Address, Port);
                Listener.Start();

                Accept(this);
            }
        }

        /// <summary>
        /// Stops the <see cref="RconServer"/>.
        /// </summary>
        /// <exception cref="SocketException">The exception is thrown by the <see cref="TcpListener"/>.</exception>
        public void Stop()
        {
            if (Listening)
            {
                Listener.Stop();
                Listener = null;
            }
        }
        #endregion

        #region Packet Handling
        static void HandlePacketReceive(object state)
        {
            ParsePacketState<RconServerClient> pState = (state as ParsePacketState<RconServerClient>);
            RconServerClient client = pState.Client;
            RconServer server = client.Parent;
            List<Packet> packets = pState.Packets;

            foreach (Packet p in packets)
            {
                if (p.IsFromServer == false && p.IsResponse == false) //is request
                {
                    //check if it's a logon or eventsenabled request
                    if (p.Words[0].CaseInsensitiveEquals("admin.eventsEnabled") && client.IsLoggedOn)
                        client.EventsEnabled = true;
                    else if (p.Words[0].CaseInsensitiveEquals("login.hashed"))
                    {
                        if (p.Words.Count == 0)
                            SendResponse(client, p, "OK", client.GetNewSalt());
                        else if (client.PasswordMatches(server.Password, p.Words[1]))
                        {
                            SendResponse(client, p, "OK");
                        }
                        else SendResponse(client, p, "InvalidPasswordHash");
                    }
                    else if (p.Words[0].CaseInsensitiveEquals("login.plainText"))
                    {
                        if (p.Words.Count > 1)
                        {
                            if (p.Words[1] == server.Password)
                            {
                                client.IsLoggedOn = true;
                                SendResponse(client, p, "OK");
                            }
                            else SendResponse(client, p, "InvalidPassword");
                        }
                        else SendResponse(client, p, "InvalidArguments");
                    }
                    else if (p.Words[0].CaseInsensitiveEquals("logout"))
                    {
                        client.IsLoggedOn = false;
                        SendResponse(client, p, "OK");
                    }
                    else if (p.Words[0].CaseInsensitiveEquals("quit"))
                    {
                        client.Disconnect(); //dont send OK because servers dont
                    }
                    else //not a built in function
                    {
                        RequestEventArgs e = new RequestEventArgs(p.Words, client.IsLoggedOn, client.Id);
                        server.OnRequest(e); //event

                        if (e.ResponseWords != null)
                            SendResponse(client, p, e.ResponseWords);
                        else
                            SendResponse(client, p, "UnknownCommand");
                    }
                }
            }
        }
        #endregion

        #region IDisposable Members
        private bool disposed = false;

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        /// <param name="disposing">Whether or not to release managed resources.</param>
        void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    Stop();
                }
            }

            disposed = true;
        }

        #endregion

    }
}
